Skip to content

Hmmrank coordinates - #72

Merged
erikrikarddaniel merged 7 commits into
nf-core:devfrom
erikrikarddaniel:hmmrank-coords-70
Aug 25, 2026
Merged

Hmmrank coordinates#72
erikrikarddaniel merged 7 commits into
nf-core:devfrom
erikrikarddaniel:hmmrank-coords-70

Conversation

@erikrikarddaniel

@erikrikarddaniel erikrikarddaniel commented Aug 24, 2026

Copy link
Copy Markdown
Member

PR checklist

  • This comment contains a description of changes (with reason).
  • If you've fixed a bug or added code that should be tested, add tests!
  • If you've added a new tool - have you followed the pipeline conventions in the contribution docs
  • If necessary, also make a PR on the nf-core/phyloplace branch on the nf-core/test-datasets repository.
  • Make sure your code lints (nf-core pipelines lint).
  • Ensure the test suite passes (nextflow run . -profile test,docker --outdir <OUTDIR>).
  • Check for unexpected warnings in debug mode (nextflow run . -profile debug,test,docker --outdir <OUTDIR>).
  • Usage Documentation in docs/usage.md is updated.
  • Output Documentation in docs/output.md is updated.
  • CHANGELOG.md is updated.
  • README.md is updated (including new tool citations and authors/contributors).

Description

Closes #70.

Adds alignment coordinates and lengths to the ranked hmmsearch summary, so a hit can be judged on how much of the profile it actually covers rather than on score alone.

Third and last PR of the chain: nf-core/test-datasets#2232 added the fixtures, nf-core/modules#12781 did the work in hmmer/hmmrank and fasta_hmmsearch_rank_fastas,
and this pulls both into the pipeline and documents them.

What changes

When --save_domtblout is set, *.hmmrank.tsv.gz gains 14 columns: tlen and qlen, plus from, to, len and n_islands for each of the hmm, ali and env coordinate sets. len is the size of the union of that hit's domains, so it drops below to - from when the domains are scattered, and n_islands counts how many separate stretches they form.

Coverage spans every domain hmmsearch reported, however weak on its own. That is deliberate: a sequence can clear the full-sequence threshold while each of its domains is poor in isolation, and those fragmented hits are exactly the ones these columns exist to expose. Which sequences are reported at all is still decided by the full-sequence scores, unchanged from before.

Without --save_domtblout the table is exactly as it was.

Also here

Two claims in AGENTS.md had HMMER_HMMRANK and both fasta_* subworkflows down as pipeline-local. All three are vendored nf-core/modules components tracked in modules.json — they originated here and are still maintained by this pipeline's author, which is what made the wording plausible, but they must be changed upstream rather than edited in place. Corrected, with a note that nf-core/ampliseq vendors two of them so changes have to stay backwards compatible.

Testing

nf-test 6/6, nf-core pipelines lint 0 failures, prek run -a clean, nextflow lint . clean at 25.10.4 and 26.04.6 (remaining findings are pre-existing on dev).

Only the ranked table's checksum moves in the pipeline snapshot — the per-profile fastas SEQTK_SUBSEQ extracts are byte-identical, confirming the new columns don't affect selection.

Both pinned to nf-core/modules 106170c99, which adds alignment coordinates
to the ranked table: hmmsearch's per-domain output is now passed on to the
ranking step, and when it is present each row gains the sequence and profile
lengths plus bounds, coverage and island count for the hmm, ali and env
coordinate sets.

Only the ranked table's checksum moves in the pipeline snapshot. The
per-profile fastas SEQTK_SUBSEQ extracts are byte-identical, so which
sequences are selected is unaffected -- the new columns are additive, and
significance is still decided by the full-sequence scores.

Generated by Claude
Describe the columns in docs/output.md, and point at them from the
--save_domtblout section of docs/usage.md, since the ranked table is an
easier place to read coordinates off than the per-domain table itself.

Also correct two claims in AGENTS.md that had HMMER_HMMRANK and both
fasta_* subworkflows down as pipeline-local. All three are vendored
nf-core/modules components tracked in modules.json; they originated here and
are still maintained by this pipeline's author, which is what made the
wording plausible. Since nf-core/ampliseq vendors hmmer/hmmrank and
fasta_hmmsearch_rank_fastas too, note that changes to them have to stay
backwards compatible.

Generated by Claude
@github-actions

Copy link
Copy Markdown

This PR is against the master branch ❌

  • Do not close this PR
  • Click Edit and change the base to dev
  • This CI test will remain failed until you push a new commit

Hi @erikrikarddaniel,

It looks like this pull-request is has been made against the erikrikarddaniel/phyloplace master branch.
The master branch on nf-core repositories should always contain code from the latest release.
Because of this, PRs to master are only allowed if they come from the erikrikarddaniel/phyloplace dev branch.

You do not need to close this PR, you can change the target branch to dev by clicking the "Edit" button at the top of this page.
Note that even after this, the test will continue to show as failing until you push a new commit.

Thanks again for your contribution!

Generated by Claude
@erikrikarddaniel
erikrikarddaniel changed the base branch from master to dev August 24, 2026 19:29
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

nf-core pipelines lint overall result: Passed ✅ ⚠️

Posted for pipeline commit d3dedca

+| ✅ 228 tests passed       |+
#| ❔  10 tests were ignored |#
!| ❗   1 tests had warnings |!
Details

❗ Test warnings:

❔ Tests ignored:

  • files_exist - File is ignored: conf/igenomes.config
  • files_exist - File is ignored: conf/igenomes_ignored.config
  • files_exist - File is ignored: .github/workflows/linting_comment.yml
  • nextflow_config - Config variable ignored: params.input
  • files_unchanged - File ignored due to lint config: .gitattributes
  • files_unchanged - File ignored due to lint config: .github/workflows/branch.yml
  • files_unchanged - File ignored due to lint config: .github/workflows/linting.yml
  • files_unchanged - File ignored due to lint config: assets/nf-core-phyloplace_logo_light.png
  • files_unchanged - File ignored due to lint config: docs/images/nf-core-phyloplace_logo_light.png
  • files_unchanged - File ignored due to lint config: docs/images/nf-core-phyloplace_logo_dark.png

✅ Tests passed:

Run details

  • nf-core/tools version 4.1.0
  • Run at 2026-08-25 16:15:09

@github-actions

Copy link
Copy Markdown

❌ nf-test failed with latest Nextflow version

Note

Tests with Nextflow's latest version failed but it will not cause a CI workflow failure.
Please check if the failure is expected with newer (edge-)releases of Nextflow or if it needs fixing.

  • singularity | latest-everything | Shard 6/6

See the full run for details.

@erikrikarddaniel
erikrikarddaniel marked this pull request as ready for review August 24, 2026 19:58

@piplus2 piplus2 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've asked Claude to look at the modifs:

Findings

1. Duplicate rank rows when an HMM file holds multiple models

Severity: medium
Location: modules/nf-core/hmmer/hmmrank/main.nf:68

domain_coords <- domains %>% distinct(accno, profile, tlen, qlen)

This assumes one row per (accno, profile), but profile is derived from the file name, and a single *.domtbl.gz can hold hits from several query profiles when the supplied HMM file contains more than one model — which this pipeline permits, since extract_hmm in the phylosearch samplesheet is optional and without it the whole HMM file goes to hmmsearch as-is.

Different models have different qlen, so distinct() emits one row per model for the same (accno, profile) key. Confirmed in R: with two qlen values for one key, distinct() returns 2 rows.

Impact. The left_join(domain_coords, by = c('accno','profile')) at lines 74/98 then multiplies every matching rank row (N tblout rows × M models), so *.hmmrank.tsv.gz gains duplicate rows. The subworkflow's filter { it.rank == '1' } → collectFile writes the same accession several times into the SEQTK_SUBSEQ id file, duplicating extracted query sequences that go on into placement. Before this PR the same input produced N rows, so this is a regression.

islands() also silently unions domains belonging to different models into one coordinate range.

Fix. Aggregate domain_coords to exactly one row per key, or join on profile_desc (the query name) as well — either closes both problems.


2. NA coordinate columns for tblout-only hits

Severity: low
Location: modules/nf-core/hmmer/hmmrank/main.nf:74

The left_join leaves all 14 new columns as NA for any (accno, profile) present in the tblout but absent from the domtblout. This is reachable with stock thresholds: hmmsearch reports a sequence in --tblout on the sequence reporting threshold, while --domtblout rows require the domain reporting threshold (--domE, default 10). A hit whose sequence score clears but whose every individual domain is above --domE yields zero domain rows.

docs/output.md and meta.yml both state each row carries the columns, with no mention of NA, and the new nf-test assertion ...isInteger() on hmm_len would fail on such a row rather than reporting it.


3. Eager output extraction masks process failures

Severity: low
Location: modules/nf-core/hmmer/hmmrank/tests/main.nf.test:78 (and :145)

def rows = path(process.out.hmmrank[0][1]).linesGzip

This is evaluated eagerly, before assert process.success inside assertAll. If the process fails, process.out.hmmrank is empty and this throws IndexOutOfBoundsException from the then block, masking the actual process failure and its log.

Fix. Move the extraction inside the assertion closures, or assert success first.


4. rows[1..-1] silently misbehaves on header-only output

Severity: low
Location: modules/nf-core/hmmer/hmmrank/tests/main.nf.test:147

rows[1..-1] on a header-only output does not throw: in Groovy, 1..-1 on a 1-element list resolves to the reverse range 1..0 and returns the header row itself, so the "known table" comparison runs against a bogus row list instead of failing cleanly. The sibling test guards this with assert rows.size() > 1 (line 90); this one does not.

Checked and correct

  • The separate() field mapping against HMMER's 23-column domtblout layout
  • The + 1L abutment rule and lag(cummax(t), default = 0L) island detection
  • Stub header column order matching the real left_join output order
  • The ^prefix\. / \.tbl\.gz$ anchoring — a strict improvement, since the unanchored .tbl.gz could match inside a profile name; the no-domtblout snapshot md5 is unchanged
  • The AGENTS.md local-vs-vendored claim: modules/local/hmmer and subworkflows/local/utils_nfcore_phyloplace_pipeline are indeed the only local components

Verification performed

The island/union arithmetic was checked in R against synthetic inputs, and the new channel plumbing (collect/ifEmpty/combine) in an actual Nextflow run — including the empty-domtblout case producing [meta, tblouts, []] and the Groovy-falsy branch in the script.

A hit is reported on the per-sequence threshold while a domain has to
clear the per-domain one, so a ranked row can have no domtblout records
behind it. Keeping NA there is intentional -- it is the clearest signal
that hmmsearch reported no domains for the hit -- so say so in the docs.

Raised in review of nf-core#72. The remaining review findings are upstream in
the vendored module and are tracked in nf-core/modules#12787.

Generated by Claude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDswY3eJxbJ9xnWSCQFVRG
@erikrikarddaniel

Copy link
Copy Markdown
Member Author

Thanks @piplus2 — useful review. I went through the findings with some help from Claude Code, and here's where each one landed.

The important structural point first: all four findings are in modules/nf-core/hmmer/hmmrank/, which is vendored from nf-core/modules and was merged there as nf-core/modules#12781. This PR does nothing but pull that in, so patching any of it here would create a local divergence that the next nf-core modules update would clobber. The fixes belong upstream.

Finding 1 — real, and worth fixing, but a corner case. Confirmed the preconditions: extract_hmm is optional in assets/schema_phylosearch_input.json (required: ['target', 'hmm']), and barrnap's arc.hmm, bac.hmm, euk.hmm and mito.hmm carry 2–4 models each, so a multi-model file reaching hmmsearch as-is is a supported configuration. The cartesian join and the cross-profile union in islands() both follow, as you describe. Two things bound the blast radius: it only fires with --save_domtblout, which is off by default, and the only no-extract_hmm row in the test data is mito_16S.hmm, which holds a single model — hence no snapshot movement. Filed as nf-core/modules#12787 with a concrete fix: the query name is already on both sides (tblout column 3, parsed as profile_desc; domtblout column 4, currently discarded as d1), so carrying it through islands() and the join closes both halves. It'll come back here as a module update.

Finding 2 — correct reading, but the NAs are intended. A sequence is reported on the per-sequence threshold while a domain has to clear the per-domain one, so a ranked row can indeed have no domain records behind it. We're keeping NA there: an empty value is the clearest available signal that hmmsearch reported no domains for that hit, and it keeps those rows distinguishable from ones whose coordinates were genuinely computed. Documented in ab4e080. The knock-on you spotted is the useful part — the module's own hmm_len.isInteger() assertion encodes "never NA", which contradicts that, so it's in #12787 too.

Finding 3 — real, minor. Agreed, lines 78 and 145 read the output before assert process.success runs. Folded into #12787.

Finding 4 — doesn't hold. I checked the Groovy behaviour rather than reasoning about it: ['header'][1..-1] throws IndexOutOfBoundsException: toIndex = 2. It does not resolve to a reverse range returning the header row, so there's no silent comparison against a bogus list — it's the same masked-failure symptom as finding 3, not a separate bug.

Your "checked and correct" list matches what I'd verified independently, including the + 1L abutment rule and the anchored ^prefix\. / \.tbl\.gz$ patterns. Nothing else to change here, so this stays a vendoring PR plus the one docs commit.

@piplus2 piplus2 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@erikrikarddaniel

Copy link
Copy Markdown
Member Author

Thanks @piplus2. Since your main point -- an important finding -- is meant to be fixed in modules, would you care to look at nf-core/modules#12788?

@piplus2

piplus2 commented Aug 25, 2026

Copy link
Copy Markdown

Ok checking in a few!

erikrikarddaniel and others added 3 commits August 25, 2026 18:06
qlen is the profile's own length, so hmm_len / qlen is the proportion of
the profile a hit covers and the hmm bounds say how much is missing from
each end. Everything needed is already on the row; the columns just did
not say what they were for.

Generated by Claude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDswY3eJxbJ9xnWSCQFVRG
tlen is the target sequence's length, so ali_len / tlen is the
proportion of the sequence the alignment takes up, the counterpart to
hmm_len / qlen for the profile. Spell out which length goes with which
coordinate set, since hmm positions live in the profile while ali and
env positions live in the sequence, and dividing across the two spaces
means nothing.

Generated by Claude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDswY3eJxbJ9xnWSCQFVRG
Pulls in nf-core/modules#12788. An HMM file may hold several models, in
which case one search puts them all in one table under one file-derived
profile name, and the coordinate columns keyed on that alone: rows were
multiplied and coordinates unioned across different models. Reachable
here because extract_hmm is optional, so a multi-model file goes to
hmmsearch as-is. Also breaks rank ties on the model name, which matters
because rank 1 is what the subworkflow selects sequences on.

Pipeline snapshot unchanged: all test HMMs that skip extract_hmm hold a
single model, and the fix is a no-op in that case.

Generated by Claude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDswY3eJxbJ9xnWSCQFVRG
@erikrikarddaniel
erikrikarddaniel merged commit 938f98c into nf-core:dev Aug 25, 2026
18 checks passed
@erikrikarddaniel
erikrikarddaniel deleted the hmmrank-coords-70 branch August 25, 2026 16:48
atrigila pushed a commit to atrigila/modules that referenced this pull request Aug 25, 2026
* hmmer/hmmrank: key domain coordinates on the query model

The profile column comes from the file name, but an HMM file may hold
several models, and the whole file is then searched in one go, so one
table can carry rows for several query names under one profile. The
coordinate columns added in nf-core#12781 keyed on (accno, profile) only, which
that breaks in two ways: distinct() returns one row per model, so the
left_join against a tblout side with one row per model becomes a
cartesian product, and islands() unions domains across different models,
which is meaningless for hmm_from/hmm_to since those are positions in
the profile.

Concatenating the two existing NrdJ fixtures into one file reproduces
it: 17 rows instead of 9, and every coordinate corrupted -- NrdJ's hit
on AACBMDAH_04658 reports NrdJm1's 1-397 rather than its own 11-95, and
AAGAFPGM_00302 gets 6-711 with hmm_len 676, a cross-model union matching
neither model.

The query name is already on both sides -- column 3 of the tblout,
parsed as profile_desc, and column 4 of the domtblout, until now
discarded as a dummy -- so carrying it through islands() and the join
fixes both halves. Output columns are unchanged: the join maps
profile_desc onto the domtblout query, so no new column is emitted and
single-model input produces byte-identical output. All existing module
and fasta_hmmsearch_rank_fastas snapshots pass untouched.

Adds a regression test that builds the multi-model case by gunzipping,
concatenating and re-gzipping the two fixtures inline, so no near
duplicate test data is needed. It fails on the previous code.

Also from the review of nf-core/phyloplace#72, both in tests/main.nf.test:
output was read in the then block before assert process.success ran, so
a process failure surfaced as an IndexOutOfBoundsException with its log
hidden; and the hmm_len assertion required an integer on every row,
ruling out the NA that a hit with no domain records legitimately gets.
NA is intended -- it is the clearest signal that hmmsearch reported no
domains for a hit -- so the assertion now allows it and meta.yml says so.

Closes nf-core#12787

Generated by Claude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDswY3eJxbJ9xnWSCQFVRG

* hmmer/hmmrank: break rank ties on the model name too

Review follow-ups on nf-core#12788.

The rank tie-break had the same root cause as the join: arrange() fell
back on profile, which is the file name, so for a file holding several
models it was constant across them and ties silently resolved to input
order. rank 1 is what downstream consumers select on, so that made the
selection arbitrary. Adding profile_desc restores the documented
alphabetical tie-break; it only takes effect where score, evalue and
profile are all equal, which cannot happen across separate files, so
single-model input is unaffected and no snapshot moves.

The hmm_len assertion allowed NA on every row, which is exactly what a
broken join produces, so it had stopped guarding the regression closest
to it. It now also requires at least one integer.

meta.yml said one row per (accno, profile) pair, stale after this PR,
and profile_desc becomes load-bearing without its name saying that it
holds the query model name. Both spelled out now.

Also close the input streams in the fixture-combining closure.

Generated by Claude

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDswY3eJxbJ9xnWSCQFVRG

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hmmrank: add alignment coordinates and lengths for all three coordinate sets

2 participants